package net.pimpas.report.processor.serviceprovider.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.pimpas.core.configuration.exception.ConfigurationException;
import net.pimpas.core.serviceprovider.ServiceProviderAnnotation;
import net.pimpas.core.serviceprovider.instancetype.ServiceProviderInstanceType;
import net.pimpas.logging.serviceprovider.LoggingServiceProvider;
import net.pimpas.logging.serviceprovider.LoggingServiceProviderFactoryImpl;
import net.pimpas.report.processor.common.CommonProcessor;
import net.pimpas.report.processor.condition.Condition;
import net.pimpas.report.processor.exception.ConditionException;
import net.pimpas.report.processor.exception.CriteriaException;
import net.pimpas.report.processor.exception.FieldValidationException;
import net.pimpas.report.processor.exception.ProjectionException;
import net.pimpas.report.processor.exception.TreeGeneratorException;
import net.pimpas.report.processor.exception.TreeScanException;
import net.pimpas.report.processor.field.Field;
import net.pimpas.report.processor.field.impl.DefaultField;
import net.pimpas.report.processor.projection.Projection;
import net.pimpas.report.processor.resolver.FieldPropertyResolver;
import net.pimpas.report.processor.serviceprovider.ReportProcessorServiceProvider;
import net.pimpas.report.processor.tree.DefaultTree;
import net.pimpas.report.processor.tree.Tree;
import net.pimpas.report.processor.tree.branch.Branch;
import net.pimpas.report.processor.tree.node.Node;
import net.pimpas.util.string.StringHelper;

import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;

@ServiceProviderAnnotation(
		instanceType=ServiceProviderInstanceType.SINGLETON,
		serviceProviderType=ReportProcessorServiceProvider.class)
public class ReportProcessorServiceProviderImpl implements ReportProcessorServiceProvider {
	
	private static final LoggingServiceProvider logger = 
		LoggingServiceProviderFactoryImpl.getLoggingServiceProvider(ReportProcessorServiceProviderImpl.class);

	public void config() throws ConfigurationException {
		
	}

	public boolean isTerminated() {
		return false;
	}

	public Tree createTree(Class<?> rootClass, String treeName) throws TreeGeneratorException {
		Tree tree = new DefaultTree(treeName, rootClass);
		try {
			tree.scanPaths();
		} catch (TreeScanException e) {
			throw new TreeGeneratorException(e);
		}
		
		return tree;
	}
	
	private void scanTree( Tree tree ) throws TreeScanException {
		if(!tree.isScanDone())
			tree.scanPaths();
	}
	
	public DetachedCriteria apply(Tree tree, List<Condition> conditions, List<Projection> projections)
		throws ConditionException, ProjectionException, FieldValidationException {
		
		this.validate(tree, conditions, projections);
		// create resolver
		final DetachedCriteria criteria = DetachedCriteria.forClass(tree.getPropertyClass());
		FieldPropertyResolver resolver = new ProcessorFieldResolver(criteria, tree);
		
		if(conditions != null)
			this.applyConditions(resolver, conditions.toArray(new Condition[conditions.size()]));
		
		if(projections != null)
			this.applyProjections(resolver, projections.toArray(new Projection[projections.size()]));
		
		return criteria;
	}
	
	private void applyConditions(FieldPropertyResolver resolver, Condition... conditions) throws ConditionException {
		logger.debug("[Apply conditions for Tree {"+resolver.getTree()+"}]");
		try {
			this.scanTree(resolver.getTree());
		} catch (TreeScanException e) {
			throw new ConditionException(e);
		}
		
		for(Condition condition: conditions) {
			// apply aliases
			if(condition.getField() != null)
				if(!resolver.getTree().isPathCorrect(condition.getField().getName()))
					throw new ConditionException(new CriteriaException("Path ["+condition.getField().getName()+"] not exists!"));
			
			logger.debug("[Inserting condition type {"+condition.getType()+"}]");
			resolver.getCriteria().add(condition.process(resolver.resolve(condition.getField()), resolver));
		}
		
		logger.debug("[All conditions applied!]");
	}

	private void applyProjections(FieldPropertyResolver resolver, Projection... projections) throws ProjectionException {
		logger.debug("[Apply Projections for Tree {"+resolver.getTree()+"}]");
		try {
			this.scanTree(resolver.getTree());
		} catch (TreeScanException e) {
			throw new ProjectionException(e);
		}
		
		for(Projection projection: projections) {
			// apply aliases
			if(projection.getField() != null)
				if(!resolver.getTree().isPathCorrect(projection.getField().getName()))
					throw new ProjectionException(new CriteriaException("Path ["+projection.getField().getName()+"] not exists!"));
			
			logger.debug("[Inserting projection type {"+projection.getType()+"}]");
			ProjectionList projectionList = Projections.projectionList();
			projectionList.add(projection.process(resolver.resolve(projection.getField()), resolver));
			resolver.getCriteria().setProjection(projectionList);
		}
		
		logger.debug("[All projections applied!]");
	}

	public Field createField(String name) {
		return new DefaultField(name);
	}
	

	public void validate(Tree tree, List<Condition> conditions, List<Projection> projections) throws FieldValidationException {
		if(tree == null) throw new NullPointerException("tree null");
		try {
			this.scanTree(tree);
		} catch (TreeScanException e) {
			throw new FieldValidationException(null, e.getMessage());
		}
		
		logger.debug("[ReportProcessor]: Executing validate method for tree ["+tree+"]");
		ProcessorFieldResolver resolver = new ProcessorFieldResolver(DetachedCriteria.forClass(tree.getPropertyClass()), tree);
		List<CommonProcessor<?, ?, ?>> all = new ArrayList<CommonProcessor<?,?,?>>();
		
		if(conditions != null)
			all.addAll(conditions);
		if(projections != null)
			all.addAll(projections);
		
		for(CommonProcessor<?, ?, ?> processor: all) {
			logger.debug("[ReportProcessor] Validating processor {"+processor+"}]");
			if(processor.getField() != null) {
				if(!resolver.getTree().isPathCorrect(processor.getField().getName())) {
					logger.error("[ReportProcessor]: Error when validating processor {"+processor+"}");
					throw new FieldValidationException(processor.getField(), "path.error");
				}
			}
			try {
				processor.validate(resolver);
			} catch(FieldValidationException e) {
				logger.error("[ReportProcessor]: Error when validating processor {"+processor+"}]", e);
				throw e;
			}
		}
	}
	
	private static final class ProcessorFieldResolver implements FieldPropertyResolver {
		
		private Map<String, String> pathMap;
		private DetachedCriteria criteria;
		private Tree tree;
		
		public ProcessorFieldResolver(DetachedCriteria criteria, Tree tree) {
			this.setPathMap(new HashMap<String, String>());
			this.setCriteria(criteria);
			this.setTree(tree);
		}
		
		public DetachedCriteria getCriteria() {
			return criteria;
		}

		public void setCriteria(DetachedCriteria criteria) {
			this.criteria = criteria;
		}

		public Tree getTree() {
			return tree;
		}

		public void setTree(Tree tree) {
			this.tree = tree;
		}

		public String resolve(Field field) {
			if(field==null) return null;
			String fieldName = field.getName();
			String[] tokens = fieldName.split("\\.");
			StringBuffer sb = new StringBuffer();
			String keyName = null;
			String foundName = null;
			if(tokens.length == 1)
				foundName = fieldName;
			else {
				for(int i=0;i<tokens.length-1;i++)
					sb.append(tokens[i]);
				keyName = (sb.length() > 0) ? sb.toString() : tokens[tokens.length-1];
				
				if(!this.getPathMap().containsKey(keyName))
					this.config(field);
				
				foundName = this.getPathMap().get(keyName);
				if(sb.length() > 0) foundName += "."+tokens[tokens.length-1];
				
			}
			
			return foundName;
		}
		
		private void config( Field field ) {
			String[] pieces = null;
			List<String> tokens = null;
			StringBuilder sb = null;
			String alias = null;
			String last = null;
			String reference = null;
			pieces = field.getName().split("\\.");
			int i=0;
			sb = new StringBuilder();
			tokens = new ArrayList<String>();
			for(String piece: pieces) {
				if(i >= pieces.length-1) break;
				tokens.add(piece);
				sb.append(piece);
				
				if(tokens.size() == 1 && !this.getPathMap().containsKey(piece)) {
					// create
					this.getPathMap().put(piece, this.createAlias(piece, piece));
					last = piece;
				} else {
					if(this.getPathMap().containsKey(piece)) {
						// just set
						last = piece;
					} else {
						// ok, what to do now?!
						// get the last
						String lastValue = this.getPathMap().get(last);
						// ok, now append the piece and create other alias
						reference = lastValue+"."+piece;
						alias = lastValue.substring(2)+"."+piece;
						last = StringHelper.join(".", this.toStringArray(tokens));
						if(!this.getPathMap().containsKey(last))
							this.getPathMap().put(last, this.createAlias(alias, reference));
					}
				}
				i++;
			}
		}
		
		public Node getNode(Node n, Field field) {
			String[] tokens = field.getName().split("\\.");
			Node node = n;
			for(int i=0;i<tokens.length-1;i++) {
				node = this.getNode((Branch)node, tokens[i]);
				if(node == null) return null;
			}
			
			return this.getNode((Branch) node, tokens[tokens.length-1]);
		}
		
		public Node getNode(Branch branch, String key) {
			for(Node node: branch.getNodes())
				if(node.getPropertyName().equals(key))
					return node;
			
			return null;
		}

		private String createAlias(String key, String reference) {
			String value = "a_"+key.replaceAll("\\.", "_");
			this.getCriteria().createAlias(reference, value);
			return value;
		}
		
		private String[] toStringArray(List<String> tokens) {
			String[] s = new String[tokens.size()];
			int i=0;
			for(String t: tokens) s[i++] = t;
			return s;
		}
		
		public Map<String, String> getPathMap() {
			return pathMap;
		}

		public void setPathMap(Map<String, String> pathMap) {
			this.pathMap = pathMap;
		}

		public Node getNode(Field field) {
			return this.getNode(this.getTree(),field);
		}
	}
}